﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml;

namespace Twitter
{
	/// <summary>
	/// Exposes operations related to the authenticated user' friends and 
	/// followers.
	/// </summary>
	public class Friendship : IFriendship
	{
		static ITraceSource tracer = Tracer.GetSourceFor<Friendship>();
		AuthenticatedEndpoint twitterEndpoint;
		BotClient client;
		Uri baseUri;

		internal Friendship(AuthenticatedEndpoint endpoint,
			Uri baseUri,
			BotClient client)
		{
			this.twitterEndpoint = endpoint;
			this.baseUri = baseUri;
			this.client = client;
		}

		/// <summary>
		/// Gets all followers.
		/// </summary>
		public IEnumerable<string> GetFollowers()
		{
			tracer.TraceInformation("Retrieving followers");
			return GetUsers("statuses/followers.xml");
		}

		/// <summary>
		/// Gets all friends.
		/// </summary>
		public IEnumerable<string> GetFriends()
		{
			tracer.TraceInformation("Retrieving friends");
			return GetUsers("statuses/friends.xml");
		}

		/// <summary>
		/// Follows the given user.
		/// </summary>
		public bool Follow(string screenName)
		{
			//http://twitter.com/friendships/create/[screenName].xml
			Uri uri = new Uri(baseUri, String.Format("friendships/create/{0}.xml", screenName));

			tracer.TraceInformation("Following {0} by invoking {1}", screenName, uri);

			try
			{
				twitterEndpoint.Execute(uri, HttpMethod.Post);

				tracer.TraceEvent(TraceEventType.Verbose, 0,
					"Relationship with friend {0} established", screenName);

				return true;
			}
			catch (WebException wex)
			{
				var response = wex.Response as HttpWebResponse;
				// We may get a Forbidden if we're already following that user.
				if (response != null && response.StatusCode == HttpStatusCode.Forbidden)
					return IsFriendOf(screenName);

				tracer.TraceError(wex, "Failed to follow {0}", screenName);
				return false;
			}
			catch (Exception ex)
			{
				tracer.TraceError(ex, "Failed to follow {0}", screenName);
				return false;
			}
		}

		/// <summary>
		/// Follows all current following users that this client is not following in turn.
		/// If an error happens while following a new friend, it will be ignored and will 
		/// not be returned as a new friend. 
		/// </summary>
		public IEnumerable<string> MakeFriends()
		{
			var notFriends = GetFollowers().Except(GetFriends());

			foreach (string notFriend in notFriends)
			{
				bool success = Follow(notFriend);
				if (success)
				{
					yield return notFriend;
				}
				else
				{
					tracer.TraceWarning("Failed to follow user {0}. Skipping from MakeFriends() results", notFriend);
				}
			}
		}

		/// <summary>
		/// Terminates the relationship with the given friend. 
		/// If an error happens, the operation is ignored.
		/// </summary>
		/// <returns><see langword="true"/> if the operation succeeded; <see langword="false"/> otherwise.</returns>
		public bool Leave(string screenName)
		{
			Guard.ArgumentNotNullOrEmptyString(screenName, "screenName");

			//http://twitter.com/friendships/destroy/[screenName].xml
			string url = String.Format("friendships/destroy/{0}.xml", screenName);

			tracer.TraceInformation("Leaving friend {0} by invoking {1}",
				screenName, new Uri(baseUri, url));

			try
			{
				twitterEndpoint.Execute(new Uri(baseUri, url), HttpMethod.Delete);

				tracer.TraceEvent(TraceEventType.Verbose, 0, "Relationship with friend {0} terminated", screenName);

				return true;
			}
			catch (Exception ex)
			{
				tracer.TraceError(ex, "Failed to leave friend {0}", screenName);
				return false;
			}
		}

		/// <summary>
		/// Stops following former friends who are not following us in turn.
		/// </summary>
		/// <returns>List of lost friends.</returns>
		public IEnumerable<string> ForgetLostFriends()
		{
			var lostFriends = GetFriends().Except(GetFollowers());

			foreach (string lostFriend in lostFriends)
			{
				bool success = Leave(lostFriend);
				if (success)
				{
					yield return lostFriend;
				}
				else
				{
					tracer.TraceWarning("Failed to leave former friend {0}. Skipping from ForgetLostFriends() results", lostFriend);
				}
			}
		}

		/// <summary>
		/// Tests whether the authenticated user is a 
		/// mutual friend with given user. Note that this 
		/// only verifies if both users are following 
		/// each other.
		/// </summary>
		public bool IsMutualFriendOf(string screenName)
		{
			Guard.ArgumentNotNullOrEmptyString(screenName, "screenName");

			var botUser = twitterEndpoint.Credentials.GetCredential(baseUri, "").UserName;

			return AreMutualFriends(botUser, screenName);
		}

		/// <summary>
		/// Tests whether the first user is a friend of the second 
		/// and viceversa.
		/// </summary>
		public bool AreMutualFriends(string firstScreenName, string secondScreenName)
		{
			Guard.ArgumentNotNullOrEmptyString(firstScreenName, "firstScreenName");
			Guard.ArgumentNotNullOrEmptyString(secondScreenName, "secondScreenName");

			return IsFriendOf(firstScreenName, secondScreenName) &&
				IsFriendOf(secondScreenName, firstScreenName);
		}

		/// <summary>
		/// Tests whether the authenticated user is a 
		/// friend of the given user, that is, whether 
		/// he's following that user.
		/// </summary>
		public bool IsFriendOf(string screenName)
		{
			Guard.ArgumentNotNullOrEmptyString(screenName, "screenName");

			var botUser = twitterEndpoint.Credentials.GetCredential(baseUri, "").UserName;

			return IsFriendOf(botUser, screenName);
		}

		/// <summary>
		/// Tests whether the first user is a friend of the second.
		/// </summary>
		public bool IsFriendOf(string screenName, string friendOf)
		{
			Guard.ArgumentNotNullOrEmptyString(screenName, "screenName");
			Guard.ArgumentNotNullOrEmptyString(friendOf, "friendOf");

			// http://twitter.com/friendships/exists.json?user_a=[screenName]&user_b=[friendOf]
			string getUrl = String.Format("friendships/exists.json?user_a={0}&user_b={1}", screenName, friendOf);

			tracer.TraceInformation("Checking whether {0} is friend of {1} by invoking {2}",
				screenName, friendOf, new Uri(baseUri, getUrl));

			try
			{
				using (var response = twitterEndpoint.CreateRequest(new Uri(baseUri, getUrl)).GetResponse())
				{
					using (var stream = response.GetResponseStream())
					{
						var result = new StreamReader(stream).ReadToEnd();
						if (result.StartsWith("\""))
							result = result.Substring(1);
						if (result.EndsWith("\""))
							result = result.Substring(0, result.Length - 1);

						return Convert.ToBoolean(result);
					}
				}
			}
			catch (Exception ex)
			{
				tracer.TraceError(ex, "Failed to test whether {0} is friend of {1} ", screenName, friendOf);
				return false;
			}
		}


		private IEnumerable<string> GetUsers(string uriString)
		{
			WebResponse response = null;
			try
			{
				WebRequest request = twitterEndpoint.CreateRequest(new Uri(baseUri, uriString));
				response = request.GetResponse();
			}
			catch (WebException wex)
			{
				// TODO: improve error parsing.
				string responseError = new StreamReader(wex.Response.GetResponseStream()).ReadToEnd();
				tracer.TraceError(wex, "Failed to retrieve users from {0}. Response was:\n{1}",
					 new Uri(baseUri, uriString),
					responseError);
				throw;
			}
			catch (Exception ex)
			{
				tracer.TraceError(ex, "Failed to retrieve users from {0}", new Uri(baseUri, uriString));
				throw;
			}
			
			using (response)
			using (Stream stream = response.GetResponseStream())
			using (XmlReader reader = XmlReader.Create(stream))
			{
				foreach (var item in PayloadReader.ReadUserScreenNames(reader))
				{
					yield return item;
				}
			}
		}
	}
}
